﻿using System;
using System.Globalization;
using System.Resources;
using System.Text.RegularExpressions;

namespace Kyoh.Lib.Component
{
    /// <summary>数値を単位接頭辞付きの文字列に変換する <see cref="ICustomFormatter"/> の実装を提供します。</summary>
    public class UnitFormatInfo : IFormatProvider, ICustomFormatter
    {
        /// <summary>数値の変換に使用する <see cref="CultureInfo"/> を取得・設定します。</summary>
        public CultureInfo Culture { get; set; }
        /// <summary>使用する単位接頭辞の種類を取得・設定します。</summary>
        public UnitPrefix Prefix { get; set; }
        /// <summary>二進単位接頭辞を使用するかどうかを取得・設定します。</summary>
        public bool Binary { get; set; }
        /// <summary>単位接頭辞の文字列表現のフォーマットを表す文字列を取得・設定します。</summary>
        public string UnitFormatString { get; set; }
        private ResourceManager _resource;
        private bool _useFullName;
        /// <summary>単位接頭辞にフルネームを使用するかどうかを取得・設定します。</summary>
        public bool UseFullName
        {
            get
            {
                return _useFullName;
            }
            set
            {
                if (_useFullName == value)
                    return;
                _resource = new ResourceManager(value ? typeof(Properties.UnitPrefixLongName) : typeof(Properties.UnitPrefixShortName));
                _useFullName = value;
            }
        }
        /// <summary><see cref="UnitFormatInfo"/> クラスの新しいインスタンスを初期化します。</summary>
        /// <param name="culture">数値の変換に使用する <see cref="CultureInfo"/>。</param>
        /// <param name="prefix">使用する単位接頭辞の種類。</param>
        /// <param name="binary">二進単位接頭辞を使用する場合は <c>true</c>。それ以外の場合は <c>false</c>。</param>
        /// <param name="fullName">単位接頭辞にフルネームを使用する場合は <c>true</c>。それ以外の場合は <c>false</c>。</param>
        /// <param name="unitFormatString">単位接頭辞の文字列表現のフォーマットを表す文字列。</param>
        public UnitFormatInfo(CultureInfo culture, UnitPrefix prefix, bool binary, bool fullName, string unitFormatString)
        {
            Culture = culture;
            Prefix = prefix;
            Binary = binary;
            _resource = new ResourceManager(fullName ? typeof(Properties.UnitPrefixLongName) : typeof(Properties.UnitPrefixShortName));
            _useFullName = fullName;
            UnitFormatString = unitFormatString;
        }

        #region IFormatProvider メンバー

        object IFormatProvider.GetFormat(Type formatType)
        {
            if (formatType == typeof(ICustomFormatter) ||
                formatType == typeof(UnitFormatInfo) ||
                formatType == typeof(NumberFormatInfo))
                return this;
            return null;
        }

        #endregion

        #region ICustomFormatter メンバー

        /// <summary>フォーマット文字列に於いて、単位接頭辞を表示する箇所を指定する文字列。</summary>
        public const string FormatUnit = "U";
        /// <summary>フォーマット文字列に於いて、数値を表示する箇所を指定する文字列。</summary>
        public const string FormatNumber = "N";
        private static readonly Regex FormatUnitRegex = new Regex(@"(?<!\\)(\\\\)*U", RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);
        private static readonly Regex FormatNumberRegex = new Regex(@"(?<!\\)(\\\\)*N", RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);

        /// <summary>
        /// 指定した書式およびカルチャ固有の書式情報を使用して、指定したオブジェクトの値をそれと等価な文字列形式に変換します。
        /// </summary>
        /// <returns>
        /// <paramref name="format"/> と <paramref name="formatProvider"/> の指定に従って書式指定した <paramref name="arg"/> の値の文字列形式。
        /// </returns>
        /// <param name="format">書式指定を格納している書式指定文字列。</param><param name="arg">書式指定するオブジェクト。</param><param name="formatProvider">現在のインスタンスについての書式情報を提供する <see cref="T:System.IFormatProvider"/> オブジェクト。</param><filterpriority>2</filterpriority>
        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            if (arg is Byte)
                return FormatDecimal(format, (Byte)arg, formatProvider);
            if (arg is UInt16)
                return FormatDecimal(format, (UInt16)arg, formatProvider);
            if (arg is UInt32)
                return FormatDecimal(format, (UInt32)arg, formatProvider);
            if (arg is UInt64)
                return FormatDecimal(format, (UInt64)arg, formatProvider);

            if (arg is SByte)
                return FormatDecimal(format, (SByte)arg, formatProvider);
            if (arg is Int16)
                return FormatDecimal(format, (Int16)arg, formatProvider);
            if (arg is Int32)
                return FormatDecimal(format, (Int32)arg, formatProvider);
            if (arg is Int64)
                return FormatDecimal(format, (Int64)arg, formatProvider);

            if (arg is Single)
                return FormatDouble(format, (Single)arg, formatProvider);
            if (arg is Double)
                return FormatDouble(format, (Double)arg, formatProvider);
            if (arg is Decimal)
                return FormatDecimal(format, (Decimal)arg, formatProvider);
            var formattable = arg as IFormattable;
            if (formattable != null)
                return (formattable).ToString(format, formatProvider);
            if (arg != null)
                return arg.ToString();
            return string.Empty;
        }

        private class Unit<T>
        {
            public Unit(UnitPrefix prefix, T unit, T binaryUnit)
            {
                Prefix = prefix;
                DecimalUnit = unit;
                BinaryUnit = binaryUnit;
            }
            public UnitPrefix Prefix { get; private set; }
            public T DecimalUnit { get; private set; }
            public T BinaryUnit { get; private set; }
            public bool IsBlower
            {
                get
                {
                    var pre = (int)Prefix;
                    return (Math.Abs(pre) > 24 && Prefix != UnitPrefix.None);
                }
            }
        }

        private static readonly Unit<double>[] DoubleUnits;
        private static readonly Unit<decimal>[] DecimalUnits;
        static UnitFormatInfo()
        {
            DoubleUnits = new[]{
                new Unit<double> (UnitPrefix.Luma, Math.Pow(1000D, 21), Math.Pow(1024D, 21)),
                new Unit<double> (UnitPrefix.Minga, Math.Pow(1000D, 20), Math.Pow(1024D, 20)),
                new Unit<double> (UnitPrefix.Nena, Math.Pow(1000D, 19), Math.Pow(1024D, 19)),
                new Unit<double> (UnitPrefix.Ocha, Math.Pow(1000D, 18), Math.Pow(1024D, 18)),
                new Unit<double> (UnitPrefix.Pepta, Math.Pow(1000D, 17), Math.Pow(1024D, 17)),
                new Unit<double> (UnitPrefix.Quexa, Math.Pow(1000D, 16), Math.Pow(1024D, 16)),
                new Unit<double> (UnitPrefix.Rinta, Math.Pow(1000D, 15), Math.Pow(1024D, 15)),
                new Unit<double> (UnitPrefix.Sorta, Math.Pow(1000D, 14), Math.Pow(1024D, 14)),
                new Unit<double> (UnitPrefix.Treda, Math.Pow(1000D, 13), Math.Pow(1024D, 13)),
                new Unit<double> (UnitPrefix.Uda, Math.Pow(1000D, 12), Math.Pow(1024D, 12)),
                new Unit<double> (UnitPrefix.Vunda, Math.Pow(1000D, 11), Math.Pow(1024D, 11)),
                new Unit<double> (UnitPrefix.Weka, Math.Pow(1000D, 10), Math.Pow(1024D, 10)),
                new Unit<double> (UnitPrefix.Xona, Math.Pow(1000D, 9), Math.Pow(1024D, 9)),
                new Unit<double> (UnitPrefix.Yotta, Math.Pow(1000D, 8), Math.Pow(1024D, 8)),
                new Unit<double> (UnitPrefix.Zeta, Math.Pow(1000D, 7), Math.Pow(1024D, 7)),
                new Unit<double> (UnitPrefix.Exa, Math.Pow(1000D, 6), Math.Pow(1024D, 6)),
                new Unit<double> (UnitPrefix.Peta, Math.Pow(1000D, 5), Math.Pow(1024D, 5)),
                new Unit<double> (UnitPrefix.Tera, Math.Pow(1000D, 4), Math.Pow(1024D, 4)),
                new Unit<double> (UnitPrefix.Giga, Math.Pow(1000D, 3), Math.Pow(1024D, 3)),
                new Unit<double> (UnitPrefix.Mega, Math.Pow(1000D, 2), Math.Pow(1024D, 2)),
                new Unit<double> (UnitPrefix.Kilo, Math.Pow(1000D, 1), Math.Pow(1024D, 1)),
                new Unit<double> (UnitPrefix.Hecto, 100D, -1),
                new Unit<double> (UnitPrefix.Deca, 10D, -1),
                new Unit<double> (UnitPrefix.None, 1D, 1D),
                new Unit<double> (UnitPrefix.Deci, 0.1D, -1),
                new Unit<double> (UnitPrefix.Centi, 0.01D, -1),
                new Unit<double> (UnitPrefix.Milli, Math.Pow(1000D, -1), -1),
                new Unit<double> (UnitPrefix.Micro, Math.Pow(1000D, -2), -1),
                new Unit<double> (UnitPrefix.Nano, Math.Pow(1000D, -3), -1),
                new Unit<double> (UnitPrefix.Pico, Math.Pow(1000D, -4), -1),
                new Unit<double> (UnitPrefix.Femt, Math.Pow(1000D, -5), -1),
                new Unit<double> (UnitPrefix.Atto, Math.Pow(1000D, -6), -1),
                new Unit<double> (UnitPrefix.Zepto, Math.Pow(1000D, -7), -1),
                new Unit<double> (UnitPrefix.Yocto, Math.Pow(1000D, -8), -1),
                new Unit<double> (UnitPrefix.Xonto, Math.Pow(1000D, -9), -1),
                new Unit<double> (UnitPrefix.Wekto, Math.Pow(1000D, -10), -1),
                new Unit<double> (UnitPrefix.Vunkto, Math.Pow(1000D, -11), -1),
                new Unit<double> (UnitPrefix.Unto, Math.Pow(1000D, -12), -1),
                new Unit<double> (UnitPrefix.Trekto, Math.Pow(1000D, -13), -1),
                new Unit<double> (UnitPrefix.Sotro, Math.Pow(1000D, -14), -1),
                new Unit<double> (UnitPrefix.Rimto, Math.Pow(1000D, -15), -1),
                new Unit<double> (UnitPrefix.Quekto, Math.Pow(1000D, -16), -1),
                new Unit<double> (UnitPrefix.Pekro, Math.Pow(1000D, -17), -1),
                new Unit<double> (UnitPrefix.Otro, Math.Pow(1000D, -18), -1),
                new Unit<double> (UnitPrefix.Nekto, Math.Pow(1000D, -19), -1),
                new Unit<double> (UnitPrefix.Mikto, Math.Pow(1000D, -20), -1),
                new Unit<double> (UnitPrefix.Lunto, Math.Pow(1000D, -21), -1)
                               };
            DecimalUnits = new[]{
                new Unit<decimal> (UnitPrefix.Xona, 1000000000000000000000000000M, 1M * 1024M * 1024M * 1024M * 1024M * 1024M * 1024M * 1024M * 1024M * 1024M),
                new Unit<decimal> (UnitPrefix.Yotta, 1000000000000000000000000M, 1M * 1024M * 1024M * 1024M * 1024M * 1024M * 1024M * 1024M * 1024M),
                new Unit<decimal> (UnitPrefix.Zeta, 1000000000000000000000M, 1M * 1024M * 1024M * 1024M * 1024M * 1024M * 1024M * 1024M),
                new Unit<decimal> (UnitPrefix.Exa, 1000000000000000000M, 1M * 1024M * 1024M * 1024M * 1024M * 1024M * 1024M),
                new Unit<decimal> (UnitPrefix.Peta, 1000000000000000M, 1M * 1024M * 1024M * 1024M * 1024M * 1024M),
                new Unit<decimal> (UnitPrefix.Tera, 1000000000000M, 1M * 1024M * 1024M * 1024M * 1024M),
                new Unit<decimal> (UnitPrefix.Giga, 1000000000M, 1M * 1024M * 1024M * 1024M),
                new Unit<decimal> (UnitPrefix.Mega, 1000000M, 1M * 1024M * 1024M),
                new Unit<decimal> (UnitPrefix.Kilo, 1000M, 1M * 1024M),
                new Unit<decimal> (UnitPrefix.Hecto, 100M, -1),
                new Unit<decimal> (UnitPrefix.Deca, 10M, -1),
                new Unit<decimal> (UnitPrefix.None, 1M, 1M),
                new Unit<decimal> (UnitPrefix.Deci, 0.1M, -1),
                new Unit<decimal> (UnitPrefix.Centi, 0.01M, -1),
                new Unit<decimal> (UnitPrefix.Milli, 0.001M, -1),
                new Unit<decimal> (UnitPrefix.Micro, 0.000001M, -1),
                new Unit<decimal> (UnitPrefix.Nano, 0.000000001M, -1),
                new Unit<decimal> (UnitPrefix.Pico, 0.000000000001M, -1),
                new Unit<decimal> (UnitPrefix.Femt, 0.000000000000001M, -1),
                new Unit<decimal> (UnitPrefix.Atto, 0.000000000000000001M, -1),
                new Unit<decimal> (UnitPrefix.Zepto, 0.000000000000000000001M, -1),
                new Unit<decimal> (UnitPrefix.Yocto, 0.000000000000000000000001M, -1),
                new Unit<decimal> (UnitPrefix.Xonto, 0.000000000000000000000000001M, -1)
                                };
        }

        private string FormatDouble(string format, double p, IFormatProvider formatProvider)
        {
            double unit = 1;
            UnitPrefix prefix = UnitPrefix.None;
            if (Prefix == UnitPrefix.SiMatric || Prefix == UnitPrefix.Blower)
            {
                if (!Binary)
                {
                    var minimum = DoubleUnits[DoubleUnits.Length - 1];
                    unit = minimum.DecimalUnit;
                    prefix = minimum.Prefix;
                }
                foreach (var u in DoubleUnits)
                {
                    if (Prefix != UnitPrefix.Blower && u.IsBlower)
                        continue;

                    var un = (Binary) ? u.BinaryUnit : u.DecimalUnit;

                    if (un < 0)
                        continue;

                    if (Math.Abs(p) >= un)
                    {
                        unit = un;
                        prefix = u.Prefix;
                        break;
                    }
                }
            }
            else
            {
                foreach (var u in DoubleUnits)
                {
                    if (u.Prefix != Prefix)
                        continue;
                    unit = (Binary) ? u.BinaryUnit : u.DecimalUnit;
                    prefix = u.Prefix;
                }
            }

            return FormatUnitRegex.Replace(FormatNumberRegex.Replace(UnitFormatString, (p / unit).ToString(format, formatProvider)), _resource.GetString(Enum.GetName(typeof(UnitPrefix), prefix), Culture));
        }

        private string FormatDecimal(string format, decimal p, IFormatProvider formatProvider)
        {
            decimal unit = 1;
            UnitPrefix prefix = UnitPrefix.None;
            if (Prefix == UnitPrefix.SiMatric || Prefix == UnitPrefix.Blower)
            {
                if (!Binary)
                {
                    var minimum = DecimalUnits[DecimalUnits.Length - 1];
                    unit = minimum.DecimalUnit;
                    prefix = minimum.Prefix;
                }
                foreach (var u in DecimalUnits)
                {
                    if (Prefix != UnitPrefix.Blower && u.IsBlower)
                        continue;

                    var un = (Binary) ? u.BinaryUnit : u.DecimalUnit;

                    if (un < 0)
                        continue;

                    if (Math.Abs(p) >= un)
                    {
                        unit = un;
                        prefix = u.Prefix;
                        break;
                    }
                }
            }
            else
            {
                foreach (var u in DecimalUnits)
                {
                    if (u.Prefix == Prefix)
                    {
                        unit = (Binary) ? u.BinaryUnit : u.DecimalUnit;
                        prefix = u.Prefix;
                    }
                }
            }

            return FormatUnitRegex.Replace(FormatNumberRegex.Replace(UnitFormatString, (p / unit).ToString(format, formatProvider)), _resource.GetString(Enum.GetName(typeof(UnitPrefix), prefix), Culture));
        }

        #endregion
    }
}
